The 20% of the JS journal + prep plan an interviewer actually probes, with runnable code for each. Study this; skim the big files only where something here feels shaky. Working reference build: react/FleetList.jsx.
1 ยท State rules โ most-tested React knowledge
- Minimal state: store only user-changed data. Winner, sunk status, filtered lists = derived in render, never stored. Storing derived state (then syncing it with useEffect) is the classic interview red flag.
- State is stale until next render. Compute with locals first, call setters at the end of the handler.
- Functional update when new depends on old:
setX(prev => ...). Multiple plain setX(x+1) calls in one handler collapse to one increment โ batching.
- Never mutate. New arrays/objects every update; React compares by reference and skips re-renders on same reference.
- useReducer when many action types hit one structure (game moves, undo): one pure
(state, action) => newState.
- Component body order: state โ derived โ handlers โ JSX.
2 ยท Rendering a grid โ the full pattern
Three parts: build the 2D state, render it with nested maps inside CSS grid, update one cell immutably. The CSS trick: the wrapper is one grid container with repeat(COLS, ...), and you render a flat stream of cells into it โ row divs are unnecessary. Keys combine row and column since grid cells have no natural id.
// 1. Build the 2D state โ Array.from gives each row its OWN array.
// Array(3).fill([]) is a bug: every row would be the same array object.
const ROWS = 3, COLS = 4;
const [cells, setCells] = useState(() =>
Array.from({ length: ROWS }, () => Array(COLS).fill('empty'))
);
// 2. Render โ outer map = rows, inner map = cells in that row.
// CSS grid wraps the flat children into COLS columns automatically.
return (
<div style={{
display: 'grid',
gridTemplateColumns: `repeat(${COLS}, 48px)`,
gap: 4,
}}>
{cells.map((row, r) =>
row.map((cell, c) => (
<button
key={`${r}-${c}`} // stable for a fixed-size grid
onClick={() => handleClick(r, c)}
style={{ height: 48, background: colorFor(cell) }}
>
{cell === 'hit' ? 'โ' : ''}
</button>
))
)}
</div>
);
// 3. Update ONE cell immutably โ copy only the row you touch.
// Untouched rows keep their reference (matters later for React.memo).
function handleClick(r, c) {
setCells(prev => prev.map((row, ri) =>
ri !== r
? row // untouched row: same reference
: row.map((cell, ci) => ci !== c ? cell : nextState(cell))
));
}
// State machine as data โ no if/else chain
const CYCLE = { empty: 'hit', hit: 'miss', miss: 'empty' };
const nextState = cell => CYCLE[cell];
- Why
useState(() => ...): the function form (lazy init) builds the array once, not on every render.
- Why not
row[r][c] = x: mutation keeps the same outer reference, React may skip the re-render โ and even when it renders, memoized children won't update.
- Say out loud: "untouched rows keep their reference, so if I memoize rows or cells later, only the changed one re-renders."
3 ยท Controlled input โ the two-way wiring
Controlled = React state is the single source of truth. The input shows exactly value; typing fires onChange, which updates state, which re-renders the input with the new value. Forget onChange and the input is frozen; forget value and it's uncontrolled (DOM owns it, read via ref).
const [query, setQuery] = useState('');
<input
value={query} // state โ UI
onChange={e => setQuery(e.target.value)} // UI โ state
placeholder="Search"
/>
// Same idea for select
<select value={status} onChange={e => setStatus(e.target.value)}>
<option value="all">All</option>
<option value="stopped">Stopped</option>
</select>
// Form submit: preventDefault or the page reloads
<form onSubmit={e => { e.preventDefault(); addItem(query); }}>
4 ยท useCallback + React.memo โ the pair
Yes โ the handler is a function, and a component recreates every function defined in its body on every render. New function = new reference = a memoized child sees a "changed" prop and re-renders anyway. useCallback returns the same function reference across renders, which is what lets React.memo actually skip work. One is useless without the other.
// Child: React.memo skips re-render if all props are === to last time
const Cell = React.memo(function Cell({ state, onClick }) {
return <button onClick={onClick}>{state}</button>;
});
// Parent โ WITHOUT useCallback: new function each render,
// so every Cell re-renders and React.memo does nothing:
const handleClick = (r, c) => { /* ... */ }; // โ new ref each render
// WITH useCallback: same reference until deps change
const handleClick = useCallback((r, c) => {
setCells(prev => updateCell(prev, r, c)); // functional update โ
}, []); // no deps needed at all
// Trap: inline arrow in JSX recreates the ref and defeats memo again.
<Cell onClick={() => handleClick(r, c)} /> // โ new ref per render
// Fix: pass r,c as props and let Cell call onClick(r, c) itself.
- Interview line: "I'd only add this after noting all 100 cells re-render per click โ at this size it's optional, and I'd measure first."
useMemo is the same idea for values; useCallback(fn, deps) โก useMemo(() => fn, deps).
5 ยท Debounce for a search input โ two ways
Debounce = fire once after calls stop for N ms (search input). Throttle = fire at most once per N ms while calls keep coming (scroll, resize, mousemove). Say aloud: "debounce waits for silence; throttle samples during noise." The React trap: the debounced fn must be created once, not on every render, or each render gets a fresh timer and the debounce never holds.
A ยท Debounce factory (lodash or hand-rolled) + useMemo
// CoderPad has npm โ lodash just works. No lodash? The hand-rolled version is 5 lines.
import { debounce } from 'lodash';
function debounce(fn, ms) { // hand-rolled โ know it cold
let timer; // timer id lives in the closure (the persistent slot)
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
// In React โ useMemo [] so the SAME debounced fn survives re-renders:
const debouncedSetQuery = useMemo(
() => debounce((value) => setQuery(value), 300),
[]);
const onSearch = (e) => {
setInputValue(e.target.value); // controlled input: instant
debouncedSetQuery(e.target.value); // filtering: after the pause
};
// cancel a pending call if the component unmounts (lodash gives .cancel())
useEffect(() => () => debouncedSetQuery.cancel(), [debouncedSetQuery]);
// โโ Why useMemo and NOT useCallback here (common instinct โ it's a function!) โโ
// useMemo CALLS its fn and stores the RETURN value; useCallback stores the fn itself.
// You want debounce()'s OUTPUT (built once), which is a value โ that's useMemo's job.
useMemo(() => debounce(setQuery, 300), []); // โ debounce() runs ONCE (inside the arrow)
useCallback(debounce(setQuery, 300), []); // โ debounce() runs EVERY render, extras thrown away; eslint warns
// useCallback is for a fn you define INLINE: useCallback((v) => setQuery(v), []) โ but that's NOT debounced.
B ยท Timer ref + useEffect cleanup (simplest to write live)
// No helper, no useMemo. ref.current holds anything (timer id, DOM node...);
// persists across renders, and writing to it does NOT re-render.
// That's why the id survives between keystrokes.
const timerRef = useRef(null);
const onSearch = (e) => {
const value = e.target.value;
setInputValue(value); // input: instant
clearTimeout(timerRef.current); // cancel PREVIOUS keystroke's timer
timerRef.current = setTimeout(() => setQuery(value), 300); // arm new, save id
};
// clear a pending timer if the component unmounts mid-countdown
useEffect(() => () => clearTimeout(timerRef.current), []);
- Which to use: B (timer ref) for one search box โ self-contained, nothing to import, easy to narrate. A (factory) when debounce is reused or the prompt is literally "implement debounce."
- Timer-ref traps (all make debounce a no-op): storing the id in a local
const timer instead of the ref โ nothing survives to cancel, every keystroke fires. Calling clearTimeout(timer) in the SAME handler on the timer you just set โ cancels it instantly, nothing ever fires. The clear must reach a timer from an earlier call, so the id needs a persistent slot (ref).
6 ยท BFS / DFS in JavaScript
Waymo scope: level-order on a tree, flood-fill on a grid. BFS = queue (shift), DFS = recursion or stack (pop). Grid version needs bounds check + visited/mark.
// BFS โ level order on a tree (returns [[level0], [level1], ...])
function levelOrder(root) {
if (!root) return [];
const result = [], queue = [root];
while (queue.length) {
const size = queue.length; // snapshot = this level
const level = [];
for (let i = 0; i < size; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level);
}
return result;
}
// DFS โ flood fill on a grid (recursion; mark visited by mutating)
const DIRS = [[0,1],[0,-1],[1,0],[-1,0]];
function flood(grid, r, c) {
if (r < 0 || r >= grid.length ||
c < 0 || c >= grid[0].length ||
grid[r][c] !== '1') return;
grid[r][c] = '0'; // mark visited
for (const [dr, dc] of DIRS) flood(grid, r + dr, c + dc);
}
queue.shift() is O(n) โ fine in interviews; mention "I'd use a head-index or deque for large n."
- Mutating the algorithm's own grid copy is fine โ just never mutate React state this way.
7 ยท reduce โ especially reduce-as-map
Signature: arr.reduce((acc, item) => newAcc, initial). The accumulator is whatever you want โ number, object, Map. "Reduce into an object" is how you group and count in JS (Python's defaultdict equivalent).
// Sum โ the trivial case
const total = nums.reduce((acc, n) => acc + n, 0);
// Count by key โ { stopped: 3, active: 2 } ("reduce as map")
const counts = vehicles.reduce((acc, v) => {
acc[v.status] = (acc[v.status] ?? 0) + 1;
return acc; // โ forgetting this = classic bug
}, {});
// Group by key โ { 'sf-1': [v1, v4], 'ph-1': [v3] }
const byZone = vehicles.reduce((acc, v) => {
(acc[v.zoneId] ??= []).push(v); // ??= creates the bucket once
return acc;
}, {});
// Index by id โ O(1) lookup table (Map keeps non-string keys)
const byId = new Map(vehicles.map(v => [v.id, v]));
- Rule of thumb to say aloud: array โ array is
map/filter; array โ one thing (object, Map, number) is reduce.
- If the reduce body needs more than ~3 lines, a plain
for...of loop is more readable โ saying that is a point in your favor.
โ Gaps โ in neither journal nor prep plan, likely to come up
- useEffect dependency arrays. Journal only covers cleanup. Know:
[] = mount once, [x] = when x changes, none = every render. For a TPS build you rarely need useEffect at all โ say "no effect needed, everything derives from state" and score points.
- Immutable nested/2D updates. Biggest practical gap given grids โ now covered in panel 2. Drill until automatic.
- Lifting state up. Two siblings need the same data โ state moves to the parent. One sentence, be able to say it.
- Index-as-key pitfall. Journal says "needs a key" but not why index breaks: on reorder/delete React reuses the wrong DOM/state. Fine only for fixed-size never-reordered grids.
- Typing React in TS. Journal has enums only. Know:
interface Props { state: Cell; onClick: () => void }, useState<Cell[][]>(...), e: React.ChangeEvent<HTMLInputElement>. (CoderPad squiggles are cosmetic; // @ts-nocheck if noisy.)
- Controlled vs uncontrolled as a concept โ panel 3 covers controlled; uncontrolled = DOM owns the value, read via
ref on submit.
- Custom hooks / useContext: mention-only. "I'd extract the debounce into a useDebouncedValue hook" is a strong closer; don't study deeply.
Focus โ what Waymo actually asks
- Grids: yes, drill them. Battleship board (reported 2026), "Random Grid Token Placement" (TPS), Set Matrix Zeroes and Sparse Matrix in the pool. Grid state + click cycling + derived status is the highest-value rehearsal โ panel 2 is that whole build in miniature.
- Equal weight: filterable data list. Fleet-monitoring role โ vehicle list with status dropdown, sort, debounced search. Full worked version:
react/FleetList.jsx + react/fleet-list-question.html. Build both once, timed, before the call.
- Skip this week: DP, backtracking, heaps, hard graphs, polyfill drills. Light BFS/DFS only (panel 6 is the ceiling).
- Rehearse the mouth, not just the hands: state shape out loud, Big-O out loud, the memoization talk track (panel 4) out loud.